/**
 * ExtDirect4Java
 * @author Maxim Egorov
 * @version 0.2-alpha
 *
 * Copyright(c) 2009, Swissdotnet S�rl
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 */
package ch.swissdotnet.extdirect4j;


import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;

import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class provides API methods to parse Ext.Direct request, lookup appropriate action/method,
 * invoke it and return result as complete Ext.Direct response.
 * Additionaly you can extend gson serialize/deserialize mechanism by registering your own type adapters.
 * <br>
 * For more info see gson documentation:
 * <a href="http://sites.google.com/site/gson/gson-user-guide#TOC-Custom-Serialization-and-Deserializ">
 * Custom Serialization and Deserialization</a>
 */
public class Processor {
    private boolean debugMode = false;
    private String remotingApiVar = "Ext.app.REMOTING_API";
    private String remoteUrl = null;
    private final GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls();
    private final Map<String, AbstractAction> actionMap = new HashMap<String, AbstractAction>();

    /**
     * Default constructor.
     */
    public Processor() {
        this(false);
    }

    /**
     * Constructor.
     *
     * @param debugMode true to switch on debug mode (default false)
     */
    public Processor(final boolean debugMode) {
        this.debugMode = debugMode;
        this.gsonBuilder.registerTypeAdapter(ExtDirectQuery.class, Utils.getQueryDeserializer(this.actionMap)).
                registerTypeAdapter(AbstractAction.class, Utils.getActionSerializer());
    }

    /**
     * Whether process has been configured.
     *
     * @return true if Processor has any configured action(s).
     */
    public boolean isConfigured() {
        return !actionMap.isEmpty();
    }

    /**
     * .
     *
     * @return true if debug mode is on
     */
    public boolean isDebugMode() {
        return debugMode;
    }

    public void setDebugMode(final boolean debugMode) {
        this.debugMode = debugMode;
    }

    /**
     * Accessor for GsonBuilder.
     *
     * @return GsonBuilder used in json serialization/deserialization
     */
    public GsonBuilder getGsonBuilder() {
        return gsonBuilder;
    }

    /**
     * This method sets JavaScript variable declaration in configuration string intended for client-side Ext.Direct.
     * (default "Ext.app.REMOTING_API")
     * <br>
     * For more info see Ext.Direct specification:
     * <a href="http://extjs.com/products/extjs/direct.php">http://extjs.com/products/extjs/direct.php</a>
     *
     * @param remotingApiVar remotingApiVar
     */
    public void setRemotingApiVar(final String remotingApiVar) {
        this.remotingApiVar = remotingApiVar;
    }

    /**
     * This method sets <code>url</code> parameter in configuration output string intended for client-side Ext.Direct.
     *
     * @param remoteUrl remote url
     */
    public void setRemoteUrl(final String remoteUrl) {
        this.remoteUrl = remoteUrl;
    }

    /**
     * Add action if it is not presented yet by an action's remote name.
     *
     * @param action action
     */
    public void addAction(final AbstractAction action) {
        final String key = action.getRemoteName();
        if (!this.actionMap.containsKey(key)) {
            this.applyAction(action);
        }
    }

    /**
     * Apply action even if it is presented yet by an action's remote name.
     *
     * @param action action
     */
    public void applyAction(final AbstractAction action) {
        if (!action.isLazyInit() && !action.isInstantiated()) {
            action.init();
        }
        action.beforeApply();
        this.actionMap.put(action.getRemoteName(), action);
    }


    /**
     * Parse Ext.Direct query as JsonObject.
     *
     * @param query as JsonObject
     * @return inner representation of Ext.Direct query
     */
    protected ExtDirectQuery parseRequest(final JsonObject query) {
        try {
            return this.gsonBuilder.create().fromJson(query, ExtDirectQuery.class);
        } catch (JsonParseException e) {
            throw new ExtDirectException("invalid request", e);
        }
    }

    /**
     * This method find action by its name.
     *
     * @param actionName action name
     * @return found method
     */
    protected AbstractAction lookupAction(final String actionName) {
        AbstractAction action = this.actionMap.get(actionName);
        if (action == null) {
            throw new ExtDirectException("invalid action '" + actionName + "' requested");
        } else {
            return action;
        }
    }

    /**
     * Process Ext.Direct request as BufferedReader.
     *
     * @param br
     * @return result response string
     * @throws IOException
     */
    public String process(final BufferedReader br) throws IOException {
        String line;
        StringBuffer sb = new StringBuffer();
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        return this.process(sb.toString());
    }

    /**
     * Process Ext.Direct request as string.
     *
     * @param queryString query as string
     * @return result response as string
     */
    public String process(final String queryString) {
        ExtDirectQuery query = null;
        try {
            final JsonElement queryElement = new JsonParser().parse(queryString);
            if (queryElement instanceof JsonArray) {
                final List<ExtDirectAnswer> answers = new ArrayList<ExtDirectAnswer>();
                for (JsonElement el : (JsonArray) queryElement) {
                    query = this.parseRequest(el.getAsJsonObject());
                    answers.add(this.process(query));
                }
                return this.gsonBuilder.create().toJson(answers);
            } else {
                query = this.parseRequest(queryElement.getAsJsonObject());
                return this.gsonBuilder.create().toJson(this.process(query));
            }
        } catch (ExtDirectException e) {
            return this.wrapDirectException(e, query, this.debugMode);
        } catch (Exception e) {
            throw new ExtDirectException(e);
        }
    }

    /**
     * Process Ext.Direct single transaction.
     *
     * @param query inner representation of Ext.Direct transaction
     * @return inner representation of Ext.Direct response
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    protected ExtDirectAnswer process(final ExtDirectQuery query) throws InvocationTargetException, IllegalAccessException {
        //common checking
        Utils.commonChecking(query);
        AbstractAction action = this.lookupAction(query.getAction());
        Object queryResult = action.invoke(query.getMethod(), query.getData());
        return new ExtDirectAnswer(query, queryResult);
    }

    /**
     * This method wraps server-side exception to exception-styled response as it specified in Ext.Direct specification.
     * <br>
     * For more info see Ext.Direct specification:
     * <a href="http://extjs.com/products/extjs/direct.php">http://extjs.com/products/extjs/direct.php</a>
     *
     * @param ex        occurred exception
     * @param query     inner representation of Ext.Direct request
     * @param debugMode if false the debugging information does not appear in resul string
     * @return result exception-styled response string
     */
    protected String wrapDirectException(final ExtDirectException ex, ExtDirectQuery query, final boolean debugMode) {
        if (query == null) {
            query = new ExtDirectQuery();
        }
        final ExtDirectAnswer answer;
        if (debugMode) {
            answer = new ExceptionAnswer(query, ex.getDebugMessage(), ex.getWhere());
        } else {
            answer = new ExtDirectAnswer(query, "");
        }
        return this.gsonBuilder.create().toJson(answer);
    }

    /**
     * This method returns complete configuration string intended for client-side Ext.Direct.
     *
     * @param url <code>url</code> parameter in configuration string.
     * @param useJsonFormat on true, will return a json api var.
     * @return
     */
    public String getRemotingApi(final String url, final boolean useJsonFormat) {
        Gson gson = this.gsonBuilder.create();
        Type actionsType = new TypeToken<Map<String, AbstractAction>>() {
        }.getType();
        if (useJsonFormat)
            return "{\"url\":\"" + (this.remoteUrl == null ? url : this.remoteUrl)
                    + "\",\"type\":\"remoting\",\"actions\":" + gson.toJson(this.actionMap, actionsType)
                    + ",\"descriptor\":\"" + this.remotingApiVar + "\"};";
        else
            return this.remotingApiVar + " = {\"url\":\"" + (this.remoteUrl == null ? url : this.remoteUrl)
                    + "\",\"type\":\"remoting\",\"actions\":" + gson.toJson(this.actionMap, actionsType) + "};";
    }
}
